﻿using System;
using System.IO;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using SDLib;

namespace SoundDesigner.Samples
{
    public class Connection
    {
        private IProductManager _productmanager;
        private CommunicationPort _port;
        private string _programmer;
        private readonly IProgress<int> _progress;
        private readonly CancellationToken _canceltoken;
        private WirelessEventHandler _eventhandler;
        private IEventHandler _sdeventhandler; 
        private ScanData ScanEvent;

        enum WirelessConnectionState
        {
            disconnecting, disconnected, connecting, connected, error
        }

        public ICommunicationAdaptor CommAdaptor { get; set; }
        public IDeviceInfo ConnectedDeviceInfo { get; set;  }
        public bool ParametersUnlocked { get; set; }

        public enum WirelessConnectStage
        {
            Scan, ScanCompleted, Connecting, Connected, Cancelled, Disconnected, Disconnecting
        }

        public WirelessConnectStage Stage { get; set; }

        public bool IsConnected { get; set; }

        public bool IsConnecting { get; set; }

        public bool VerifyWrites { get; set; }

        public CancellationToken Canceltoken => _canceltoken;

        public async Task CloseWirelessConnection()
        {
            if (IsConnected)
            {
                try
                {
                    TestContext.Progress.WriteLine($"Disconnecting {ScanEvent.DeviceName}..");
                    Stage = WirelessConnectStage.Disconnecting;
                    _eventhandler.ConnectionEvent += _eventhandler_ConnectEvent;
                    CommAdaptor.Disconnect();
                    var stopwatch = Stopwatch.StartNew();
                    while (stopwatch.ElapsedMilliseconds < Constants.ConnectTimeMs)
                    {
                        await Task.Delay(200);
                        if (Stage == WirelessConnectStage.Disconnected || Stage == WirelessConnectStage.Cancelled)
                        {
                            break;
                        }
                    }

                    if (Stage != WirelessConnectStage.Disconnected)
                    {
                        TestContext.Progress.WriteLine($"Disconnect timed out, time elapsed: {stopwatch.ElapsedMilliseconds} ms");
                        throw new Exception($"Device not disconnected: {Constants.WirelessDeviceName}");
                    }
                    _eventhandler.ConnectionEvent -= _eventhandler_ConnectEvent;
                    stopwatch.Stop();
                    TestContext.Progress.WriteLine($"Wireless device disconnected");
                }
                catch (Exception e)
                {
                    _eventhandler.ConnectionEvent -= _eventhandler_ConnectEvent;
                    if (e is ApplicationException)
                        throw new Exception($"Failure to disconnect: {GetDetailedErrorStringFromName(e.Message)}");
                    else
                        throw new Exception($"Failure to disconnect: {e.Message}");
                }
            }
            _eventhandler.Stop();
        }

        public async Task Close()
        {

            #pragma warning disable CS0162
            if (Constants.IsProgrammerWireless)
            {
                await CloseWirelessConnection(); 
            }

            CommAdaptor?.CloseDevice(); // does not currently disconnect wireless, but do not call if product.closedevice is called first.
                                        // this code calls product.closedevice in the teardown of all tests 

            IsConnected = false;
            IsConnecting = false;
            #pragma warning restore CS0162
        }

        public async Task<bool> Open()
        {
            CommAdaptor = null;
            #pragma warning disable CS0162
            if (Constants.IsProgrammerWireless)
            {
                if (_programmer == "Noahlink")
                {
                    var test = Environment.ExpandEnvironmentVariables(Constants.DriverLocation);
                    _productmanager.BLEDriverPath = Path.GetFullPath(test);
                    TestContext.Progress.WriteLine($"Specified BLE driver location: {_productmanager.BLEDriverPath}");
                }

                _eventhandler = new WirelessEventHandler(_sdeventhandler =_productmanager.GetEventHandler());
                _eventhandler.Start();
                await Task.Run(() => Scan());
                await Task.Run(() => WirelessConnect()); 
                return true; 
            }
            else
            {
                try
                {
                    CommAdaptor = _productmanager.CreateCommunicationInterface(_programmer, _port, "");
                    CommAdaptor.VerifyNvmWrites = VerifyWrites;
                }
                catch (Exception e)
                {
                    throw new Exception($"Failure to create an interface for the specified programmer: {_programmer} {e.Message}");
                }
                return true;
            }
            #pragma warning restore CS0162
        }

        public async Task WirelessConnect()
        {
            try
            {
                Stage = WirelessConnectStage.Connecting;
                _eventhandler.ConnectionEvent += _eventhandler_ConnectEvent;
                TestContext.Progress.WriteLine($"Creating comm interface with {ScanEvent.DeviceName} using id: {ScanEvent.DeviceID}");
                var commAdaptor = _productmanager.CreateWirelessCommunicationInterface(ScanEvent.DeviceID);
                commAdaptor.SetEventHandler(_sdeventhandler);
                TestContext.Progress.WriteLine($"Connecting..");
                commAdaptor.Connect();
                var stopwatch = Stopwatch.StartNew();
                while (stopwatch.ElapsedMilliseconds < Constants.ConnectTimeMs)
                {
                    await Task.Delay(200);

                    if (Stage == WirelessConnectStage.Connected || Stage == WirelessConnectStage.Cancelled)
                    {
                        break;
                    }
                }
                _eventhandler.ConnectionEvent -= _eventhandler_ConnectEvent;
                stopwatch.Stop();
                if (Stage != WirelessConnectStage.Connected)
                {
                    TestContext.Progress.WriteLine($"Connect timed out, time elapsed: {stopwatch.ElapsedMilliseconds} ms");
                    throw new Exception($"Device not connected: {Constants.WirelessDeviceName}");
                }
                CommAdaptor = commAdaptor;
                CommAdaptor.VerifyNvmWrites = VerifyWrites;
                TestContext.Progress.WriteLine($"Connection established");
            }
            catch (Exception e)
            {
                _eventhandler.ConnectionEvent -= _eventhandler_ConnectEvent;
                if (e is ApplicationException)
                    throw new Exception($"Failure to connect: {GetDetailedErrorStringFromName(e.Message)}");
                else
                    throw new Exception($"Failure to connect: {e.Message}");
            }

        }
        
        private void _eventhandler_ConnectEvent(object sender, ConnectEventHandlerArgs e)
        {
            if (Stage == WirelessConnectStage.Connecting)
            {
                var ea = (ConnectEventHandlerArgs)e;
                var sevent = ea.ParseEventArgs();
                if (sevent.DeviceID == ScanEvent.DeviceID)
                {
                    TestContext.Progress.WriteLine($"Connection event rx {sevent.ConnectionState} from {sevent.DeviceID} (expected)");
                    var cs = (WirelessConnectionState)Convert.ToInt32(sevent.ConnectionState);
                    switch (cs)
                    {
                        case WirelessConnectionState.connecting:
                            //expected
                            break;
                        case WirelessConnectionState.connected:
                            Stage = WirelessConnectStage.Connected;
                            break;
                        case WirelessConnectionState.error:
                        case WirelessConnectionState.disconnecting:
                        case WirelessConnectionState.disconnected:
                        default:
                            // all unexpected 
                            Stage = WirelessConnectStage.Cancelled;
                            break;
                    }
                    if (Stage == WirelessConnectStage.Cancelled)
                        throw new Exception($"Unexpected disconnect/error {sevent.ErrorDesc}");
                }
            }
            else if ( Stage == WirelessConnectStage.Connected )
            {
                // check for disconnected
                var ea = (ConnectEventHandlerArgs)e;
                var sevent = ea.ParseEventArgs();
                if (sevent.DeviceID == ScanEvent.DeviceID)
                {
                    TestContext.Progress.WriteLine($"Connection event rx {sevent.ConnectionState} from {sevent.DeviceID}");
                    var cs = (WirelessConnectionState)Convert.ToInt32(sevent.ConnectionState);
                    switch (cs)
                    {
                        case WirelessConnectionState.connecting:
                        case WirelessConnectionState.connected:
                            // don't care
                            break;
                        case WirelessConnectionState.error:
                        case WirelessConnectionState.disconnecting:
                        case WirelessConnectionState.disconnected:
                        default:
                            // all unexpected 
                            Stage = WirelessConnectStage.Disconnected;
                            break;
                    }
                }

                if (Stage == WirelessConnectStage.Disconnected)
                {
                    TestContext.Progress.WriteLine($"Connection event rx {sevent.ConnectionState} from {sevent.DeviceID} (unexpected)");
                    throw new Exception($"Unexpected disconnect {sevent.ErrorDesc}");
                }
            }
            else if ( Stage == WirelessConnectStage.Disconnecting)
            {
                var ea = (ConnectEventHandlerArgs)e;
                var sevent = ea.ParseEventArgs();
                if (sevent.DeviceID == ScanEvent.DeviceID)
                {
                    TestContext.Progress.WriteLine($"Connection event rx {sevent.ConnectionState} from {sevent.DeviceID}");
                    var cs = (WirelessConnectionState)Convert.ToInt32(sevent.ConnectionState);
                    switch (cs)
                    {
                        case WirelessConnectionState.disconnecting:
                            // don't care
                            break;
                        case WirelessConnectionState.disconnected:
                            Stage = WirelessConnectStage.Disconnected;
                            break;
                        case WirelessConnectionState.connecting:
                        case WirelessConnectionState.connected:
                        case WirelessConnectionState.error:
                        default:
                            // all unexpected
                            Stage = WirelessConnectStage.Cancelled;
                            break;
                    }
                }
                if (Stage == WirelessConnectStage.Cancelled)
                {
                    TestContext.Progress.WriteLine($"Connection event rx {sevent.ConnectionState} from {sevent.DeviceID} (unexpected)");
                    throw new Exception($"Unexpected disconnect {sevent.ErrorDesc}");
                }
            }
        } 

        public async Task Scan()
        {
            try 
            {
                Stage = WirelessConnectStage.Scan;
                _eventhandler.ScanEvent += _eventhandler_ScanEvent;
                TestContext.Progress.WriteLine($"Starting scan");

                WirelessProgrammerType wirelessProgrammer;
                string wirelessProgrammerComPort = ""; 

                if (_programmer == "RSL10")
                {
                    wirelessProgrammer = WirelessProgrammerType.kRSL10;
                    wirelessProgrammerComPort = Constants.COMPort;
                }
                else
                {
                    wirelessProgrammer = WirelessProgrammerType.kNoahlinkWireless;
                }

                var monitor = _productmanager.BeginScanForWirelessDevices(wirelessProgrammer, wirelessProgrammerComPort, CommunicationPort.kLeft, "", false);
                var stopwatch = Stopwatch.StartNew();
                while (stopwatch.ElapsedMilliseconds < Constants.ScanTimeMs)
                {
                    await Task.Delay(200);
                    if (Stage == WirelessConnectStage.ScanCompleted)
                    {
                        break;
                    }
                }
                _eventhandler.ScanEvent -= _eventhandler_ScanEvent;
                stopwatch.Stop();
                if (Stage != WirelessConnectStage.ScanCompleted)
                {
                    TestContext.Progress.WriteLine($"Scan timed out, time elapsed: {stopwatch.ElapsedMilliseconds} ms");
                    _productmanager.EndScanForWirelessDevices(monitor); 
                    throw new Exception($"Device not found: {Constants.WirelessDeviceName}");
                }
                monitor.GetResult(); 
                _productmanager.EndScanForWirelessDevices(monitor); // no need to get the scan list as we monitored scan events. 
            }
            catch (Exception e)
            {
                _eventhandler.ScanEvent -= _eventhandler_ScanEvent;
                if (e is ApplicationException)
                    throw new Exception($"Failure to scan: {GetDetailedErrorStringFromName(e.Message)}"); 
                else
                    throw new Exception($"Failure to scan: {e.Message}");
            }
        }

        private void _eventhandler_ScanEvent(object sender, EventArgs e)
        {
            if (Stage == WirelessConnectStage.Scan)
            {
                // find the device we are looking for 
                var ea = (ScanEventHandlerArgs)e;
                var sevent = ea.ParseEventArgs();
                TestContext.Progress.WriteLine($"Scan event rx {sevent.DeviceName}");
                if (sevent.DeviceName == Constants.WirelessDeviceName)
                {
                    ScanEvent = sevent;
                    Stage = WirelessConnectStage.ScanCompleted;
                }
            }
        }

        public void UpdateProgress(int pval)
        {
            _progress?.Report(pval);
        }
        
        public async Task GetDeviceInformationBlocking()
        {
            try
            {
                var monitor = CommAdaptor.BeginDetectDevice();
                var lastpr = -1 ; 
                while (!monitor.IsFinished)
                {                    
                    await Task.Delay(300);
                    var maxsteps = monitor.ProgressMaximum;
                    var pr = monitor.GetProgressValue();
                    if (maxsteps > 0 && lastpr != pr )
                    {
                       lastpr = pr; 
                       UpdateProgress((int)((((double)pr / maxsteps) * ((double)100))));
                    }
                }
                monitor.GetResult(); // error check
                ConnectedDeviceInfo = CommAdaptor.EndDetectDevice(monitor);
            }
            catch 
            {
                ConnectedDeviceInfo = null;
                throw; 
            }
        }

        public void UnlockParameters ()
        {
            TestContext.Progress.WriteLine("Parameters are locked, unlocking with key in Constants.cs");
            var key = TextUtil.ConvertHexStringToIntArray(Constants.ParameterLockKey, 96, 4);
            if (key != null)
            {
                // TODO: The SDK is missing the async version of this call
                CommAdaptor.UnlockParameterAccess(key[0], key[1], key[2], key[3]); // will throw exception if incorrect
            }
            else
            {
                throw new InvalidOperationException("Improperly formatted Parameter Lock Key");
            }
        }

        public string GetDetailedErrorStringFromName(string errorName)
        {
            return _productmanager.GetDetailedErrorStringFromName(errorName);
        }
        
        public Connection(CommunicationPort port, IProductManager productmanager, string programmer =null, 
                          CancellationToken canceltoken = default(CancellationToken), IProgress<int> progress = null)
        {
            _programmer = programmer; 
            _productmanager = productmanager;
            _port = port; 
            IsConnected = false;
            IsConnecting = false;
            ParametersUnlocked = false;
            VerifyWrites = false;
            Stage = WirelessConnectStage.Disconnected;
            _progress = progress;
            _canceltoken = canceltoken; 
        }
    }
}
